package com.ouyunc.oauth2.config.override;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.ouyunc.common.constant.Auth2Constant;
import com.ouyunc.common.constant.RedisKeyConstant;
import com.ouyunc.oauth2.base.BaseUser;
import com.ouyunc.oauth2.service.IUserDetailsService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsPasswordService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetails;
import org.springframework.util.Assert;

import java.util.Map;

/**
 * @Author fangzhenxun
 * @Description 注意：只针对密码模式,生成token使用
 * 自定义认证provider ,由于DaoAuthenticationProvider中的关键方法 retrieveUser() 使用final修饰（final修饰的方法可以被继承和重载，但不能被重写）
 * 所以将DaoAuthenticationProvider 代码复制出来然后重写 retrieveUser里面的逻辑
 * @Date 2020/4/22 11:05
 **/
public class IUsernamePasswordAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {

    private RedisTemplate<String, Object> redisTemplate ;

    // ~ Static fields/initializers
    // =====================================================================================

    /**
     * The plaintext password used to perform
     * PasswordEncoder#matches(CharSequence, String)}  on when the user is
     * not found to avoid SEC-2056.
     */
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";

    // ~ Instance fields
    // ================================================================================================
    //可以进行修改进行多方登陆：如手机号验证码，微信登陆，扫码等方式
    private PasswordEncoder passwordEncoder;

    /**
     * The password used to perform
     * {@link PasswordEncoder#matches(CharSequence, String)} on when the user is
     * not found to avoid SEC-2056. This is necessary, because some
     * {@link PasswordEncoder} implementations will short circuit if the password is not
     * in a valid format.
     */
    private volatile String userNotFoundEncodedPassword;

    /**
     * 这里的userDetailsService 替换成自定义的userDetailsService
     **/
    private IUserDetailsService iuserDetailsService;

    /**
     * 这里的新增加客户端详情，用来处理客户端的数据信息
     */
    private ClientDetailsUserDetailsService clientDetailsUserDetailsService;

    private UserDetailsPasswordService userDetailsPasswordService;

    public IUsernamePasswordAuthenticationProvider() {
        setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
    }

    // ~ Methods
    // ========================================================================================================
    /**
     * @Author fangzhenxun
     * @Description  校验密码有效性(比如是否过期，是否锁定等).，做密码的校验,同时可以做手机号验证码验证
     * 如果想做点额外的检查，或其他模式的登陆校验,可以在这个方法里处理,校验不通时,直接抛异常即可
     * 可以做sso 直接不需要验证密码来进行登陆
     * @Date 2020/4/28 15:47
     * @param userDetails
     * @param authentication
     * @return void
     **/
    @Override
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
        }
        //获得当前凭证，可能是密码，也可能是手机号验证码
        String presentedPrincipal = authentication.getPrincipal().toString().trim();
        String presentedCredentials = authentication.getCredentials().toString().trim();
        //授权码的几种表单登陆的验证
        if (authentication.getDetails() instanceof WebAuthenticationDetails) {
            IWebAuthenticationDetails webAuthenticationDetails = (IWebAuthenticationDetails)authentication.getDetails();
            //手机号验证码
            if (Auth2Constant.FORM_LOGIN_TYPE_VERIFICATION_CODE.equals(webAuthenticationDetails.getLoginType())) {
                Object oldVerificationCode = redisTemplate.opsForValue().get(RedisKeyConstant.USER_PHONE_VERIFICATION_CODE + presentedPrincipal);
                if (!presentedCredentials.equals(oldVerificationCode)) {
                    logger.debug("Authentication failed: password does not match stored value");
                    throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","手机号或验证码不正确！"));
                }
            } else {
                //用户名密码校验
                if (!passwordEncoder.matches(presentedCredentials, userDetails.getPassword())) {
                    logger.debug("Authentication failed: password does not match stored value");
                    throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
                }
            }
        } else {
            //@todo 用户名密码校验,包括客户端信息的密码校验，这里应该加上用户名
            if (!passwordEncoder.matches(presentedCredentials, userDetails.getPassword())) {
                logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials","Bad credentials"));
            }
        }
    }

    @Override
    protected void doAfterPropertiesSet() throws Exception {
        Assert.notNull(this.iuserDetailsService, "A UserDetailsService must be set");
    }

    /**
     * @Author fangzhenxun
     * @Description   在客户端认证成功过后，密码模式/授权码模式都会走这个， 检索用户，通过该方法来对token进行处理
     * @Date 2020/4/22 11:18
     * @param username
     * @param authentication
     * @return org.springframework.security.core.userdetails.UserDetails
     **/
    @Override
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        prepareTimingAttackProtection();
        // 判断是客户端认证还是客户端下的用户名密码认证
        // 自定义添加
        UserDetails loadedUser = null;
           try {
               // ===========================在到达controller 层之前的客户端信息认证========================================
               // @TODO 注意：这个类可以在写一个用来处理客户端之前的逻辑，或者找出客户端信息和用户信息的之间的区别,我这里重写了一个认证客户端的

                //===========================在controller 中的 用户名密码包括授权码的认证===============================
               //在授权码模式中，也可以定义额外参数
               if (authentication.getDetails() instanceof WebAuthenticationDetails) {
                   loadedUser = this.getUserDetailsService().loadUserByUsername(username);
               } else {
                   Map<String,Object> authenticationDetailsMap = (Map<String, Object>) authentication.getDetails();
                   //自定义添加,扩展字段
                   Map<String,Object> extendMap =  JSON.parseObject((String) authenticationDetailsMap.get(Auth2Constant.EXTEND_MAP), Map.class);
                   //将扩展数据放进去
                   loadedUser = this.getUserDetailsService().loadUserByUsername(username, extendMap);
               }
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }


    /**
     * @Author fangzhenxun
     * @Description  自定义认证逻辑，授权码模式会走这里
     * @Date 2020/4/28 17:15
     * @param authentication
     * @return org.springframework.security.core.Authentication
     **/
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        return super.authenticate(authentication);
    }

    /**
     * @Author fangzhenxun
     * @Description 判断成功后创建认证相关信息,很关键，这里可以改变写入redis中的数据
     * @param principal
     * @param authentication
     * @param user
     * @return org.springframework.security.core.Authentication
     */
    @Override
    protected Authentication createSuccessAuthentication(Object principal,
                                                         Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService != null
                && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        ((BaseUser)principal).setPassword(null);
        // 这里将principal 对象进行JSON格式化，方便在其他服务获取
        return super.createSuccessAuthentication(JSON.toJSONString(principal, SerializerFeature.WriteMapNullValue), authentication, user);
    }

    private void prepareTimingAttackProtection() {
        if (this.userNotFoundEncodedPassword == null) {
            this.userNotFoundEncodedPassword = this.passwordEncoder.encode(USER_NOT_FOUND_PASSWORD);
        }
    }

    private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
        if (authentication.getCredentials() != null) {
            String presentedPassword = authentication.getCredentials().toString();
            this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
        }
    }

    /**
     * Sets the PasswordEncoder instance to be used to encode and validate passwords. If
     * not set, the password will be compared using {@link PasswordEncoderFactories#createDelegatingPasswordEncoder()}
     *
     * @param passwordEncoder must be an instance of one of the {@code PasswordEncoder}
     * types.
     */
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.passwordEncoder = passwordEncoder;
        this.userNotFoundEncodedPassword = null;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return passwordEncoder;
    }

    public ClientDetailsUserDetailsService getClientDetailsUserDetailsService() {
        return clientDetailsUserDetailsService;
    }

    public void setClientDetailsUserDetailsService(ClientDetailsUserDetailsService clientDetailsUserDetailsService) {
        this.clientDetailsUserDetailsService = clientDetailsUserDetailsService;
    }

    public IUserDetailsService getUserDetailsService() {
        return iuserDetailsService;
    }

    public void setUserDetailsService(IUserDetailsService userDetailsService) {
        this.iuserDetailsService = userDetailsService;
    }

    public void setUserDetailsPasswordService(
            UserDetailsPasswordService userDetailsPasswordService) {
        this.userDetailsPasswordService = userDetailsPasswordService;
    }

    public RedisTemplate<String, Object> getRedisTemplate() {
        return redisTemplate;
    }

    public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }
}
